package com.vision.core.sv.util;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Provides utility methods for dealing with collections.
 *
 * @author Mark
 *
 */
public class CollectionUtils {

	private static final Logger log = LoggerFactory.getLogger(CollectionUtils.class);
	
	public static List<Integer> intsFromString(String string, char separator) {
		if (string == null || string.isEmpty())
			return Collections.emptyList();
		
		String[] split = string.split(String.valueOf(separator));
		List<Integer> list = new ArrayList<Integer>(split.length);
		for (String s : split) {
			try {
				int l = Integer.parseInt(s);
				list.add(l);
			} catch (Exception e) {
				log.trace(e.getMessage(), e);
			}
		}
		return list;
	}
	
	public static List<Long> longsFromString(String string, char separator) {
		if (string == null || string.isEmpty())
			return Collections.emptyList();
		
		String[] split = string.split(String.valueOf(separator));
		List<Long> list = new ArrayList<Long>(split.length);
		for (String s : split) {
			try {
				long l = Long.parseLong(s);
				list.add(l);
			} catch (Exception e) {
				log.trace(e.getMessage(), e);
			}
		}
		return list;
	}

	public static boolean isNullOrEmpty(Collection<?> c) {
		return c == null || c.isEmpty();
	}
	
	
	/**
	 * Creates a {@link List} and copies the elements in the <tt>array</tt> to the list.
	 * If <tt>array</tt> is null or empty, the method returns an empty list.
	 *
	 * @param <T>
	 * @param array - the source array
	 * @return
	 */
	public static <T> List<T> asList(T[] array) {
		if (array == null || array.length == 0)
			return new ArrayList<T>(0); // Collections.emptyList() is not compatible with GWT

		List<T> list = new ArrayList<T>(array.length);
		for (T item : array)
			list.add(item);

		return list;
	}

	/**
	 * Creates a {@link Set} and copies the elements in the <tt>array</tt> to the set.
	 * If <tt>array</tt> is null or empty, the method returns an empty set.
	 *
	 * @param <T>
	 * @param array - the source array
	 * @return
	 */
	public static <T> Set<T> asSet(T[] array) {
		if (array == null || array.length == 0)
			return new HashSet<T>(0); // Collections.emptyList() is not compatible with GWT

		Set<T> set = new HashSet<T>(array.length);
		for (T item : array)
			set.add(item);

		return set;
	}

	/**
	 * Creates a {@link Map} from a {@link List} of objects, with the resulting map's
	 * keys determined by <tt>keyMethod</tt> and the objects
	 * in the list as the values in the map. If <tt>list</tt> is null or empty, the
	 * method returns an empty map.
	 *
	 * The method uses the Java Reflection API.
	 *
	 * @param <K>
	 * @param <T>
	 * @param list - the list of objects to copy into a map
	 * @param keyMethod - the name of the method used to obtain a reference to the object that will be used as key
	 * 						for each element.
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <K, T> Map<K, T> mapBy(Collection<T> list, String keyMethod) {
		if (list == null || list.isEmpty())
			return new HashMap<K, T>(0); // Collections.emptyMap() is not compatible with GWT

		Method getKey = getMethod(list.iterator().next(), keyMethod);

		Map<K, T> map = new HashMap<K, T>(list.size());
		// Now process each item in the list
		for (T obj : list) {
			try {
				// Invoke method to get the key
				K key = (K) getKey.invoke(obj, (Object[]) null);
				if (key == null) {
					log.trace("Retrieved key is null. Object will be ignored.");
				} else {
					map.put(key, obj);
				}
			} catch (IllegalArgumentException e) {
				log.warn(e.getMessage());
			} catch (IllegalAccessException e) {
				log.warn(e.getMessage());
			} catch (InvocationTargetException e) {
				log.warn(e.getMessage());
			}
		}

		return map;
	}

	@SuppressWarnings("unchecked")
	public static <K, T> Map<K, T> mapBy(T[] array, String keyMethod) {
		if (array == null || array.length == 0)
			return new HashMap<K, T>(0); // Collections.emptyMap() is not compatible with GWT

		Method getKey = getMethod(array[0], keyMethod);

		Map<K, T> map = new HashMap<K, T>(array.length);
		// Now process each item in the list
		for (T obj : array) {
			try {
				// Invoke method to get the key
				K key = (K) getKey.invoke(obj, (Object[]) null);
				if (key == null) {
					log.trace("Retrieved key is null. Object will be ignored.");
				} else {
					map.put(key, obj);
				}
			} catch (IllegalArgumentException e) {
				log.warn(e.getMessage());
			} catch (IllegalAccessException e) {
				log.warn(e.getMessage());
			} catch (InvocationTargetException e) {
				log.warn(e.getMessage());
			}
		}

		return map;
	}

	/**
	 * Creates a {@link Map} from a {@link List} of objects, with the resulting map's
	 * keys constructed from the returned values of the <tt>keyMethods</tt> delimited by <tt>delimter</tt>
	 * and the objects in the list as the values in the map. If <tt>list</tt> is null or empty, the
	 * method returns an empty map.
	 *
	 * The method uses the Java Reflection API.
	 *
	 * @param <T>
	 * @param list
	 * @param delimiter - the delimiter used to separate each component of the key
	 * @param keyMethods - array of methods that returns components of the key that is constructed
	 * @return
	 */
	public static <T> Map<String, T> mapBy(Collection<T> list, String delimiter, String ... keyMethods) {
		if (list == null || list.isEmpty())
			return new HashMap<String, T>(0); // Collections.emptyMap() is not compatible with GWT

		Method[] getKeys = new Method[keyMethods.length];

		try {
			// Try to get a reference to the method that will return the key for each object
			T obj = list.iterator().next();
			Class<?> clazz = obj.getClass();

			if (log.isDebugEnabled())
				log.debug("Processing list of " + clazz.getName() + " objects. Key methods: " + Arrays.toString(keyMethods));

			int i = 0;
			for (String keyMethod : keyMethods)
				getKeys[i++] = clazz.getMethod(keyMethod, (Class<?>[]) null);
		} catch (SecurityException e) {
			throw new RuntimeException(e);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(e);
		}

		Map<String, T> map = new HashMap<String, T>(list.size());
		// Now process each item in the list
		for (T obj : list) {
			try {
				StringBuilder keyBuffer = new StringBuilder();
				int i = 0;
				for (; i < getKeys.length; i++) {
					Method getKey = getKeys[i];
					Object key = getKey.invoke(obj, (Object[]) null);
					if (key == null) {
						log.trace("Retrieved key (method=" + getKey.getName() +") is null. Object will be ignored.");
						break;
					}

					keyBuffer.append(key);
					if (i < getKeys.length - 1) // Do not append the delimiter at the end of the string
						keyBuffer.append(delimiter);
				}

				// We did not reach the end of the list, meaning some method returned null
				if (i < getKeys.length)
					continue;

				map.put(keyBuffer.toString(), obj);
			} catch (IllegalArgumentException e) {
				log.warn(e.getMessage());
			} catch (IllegalAccessException e) {
				log.warn(e.getMessage());
			} catch (InvocationTargetException e) {
				log.warn(e.getMessage());
			}
		}

		return map;
	}

	@SuppressWarnings("unchecked")
	public static <T, O> List<O> listFrom(Collection<T> list, String method) {
		if (list == null || list.isEmpty())
			return new ArrayList<O>(0);

		Method m = getMethod(list.iterator().next(), method);

		List<O> resultingList = new ArrayList<O>(list.size());

		for (T obj : list) {
			try {
				// Invoke method to get the key
				O value = (O) m.invoke(obj, (Object[]) null);
				if (value == null) {
					log.trace("Retrieved value is null. Object will be ignored.");
				} else {
					resultingList.add(value);
				}
			} catch (IllegalArgumentException e) {
				log.warn(e.getMessage());
			} catch (IllegalAccessException e) {
				log.warn(e.getMessage());
			} catch (InvocationTargetException e) {
				log.warn(e.getMessage());
			}
		}

		return resultingList;
	}

	@SuppressWarnings("unchecked")
	public static <T, O> Set<O> setFrom(Collection<T> list, String method) {
		if (list == null || list.isEmpty())
			return new HashSet<O>(0);

		Method m = getMethod(list.iterator().next(), method);

		Set<O> set = new HashSet<O>(list.size());

		for (T obj : list) {
			try {
				// Invoke method to get the key
				O value = (O) m.invoke(obj, (Object[]) null);
				if (value == null) {
					log.warn("Retrieved value is null. Object will be ignored.");
				} else {
					set.add(value);
				}
			} catch (IllegalArgumentException e) {
				log.warn(e.getMessage());
			} catch (IllegalAccessException e) {
				log.warn(e.getMessage());
			} catch (InvocationTargetException e) {
				log.warn(e.getMessage());
			}
		}

		return set;
	}

	/**
	 * Groups the elements in <tt>list</tt> based on the return value of <tt>keyMethod</tt>
	 * when invoked on each element. The result is a mapping between the unique return values
	 * of each <tt>keyMethod</tt> invocation (the key) and a list of elements who returned
	 * the value when <tt>keyMethod</tt> was invoked on them.
	 *
	 * @param <T>
	 * @param <K>
	 * @param list
	 * @param keyMethod
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <T, K> Map<K, List<T>> group(Collection<T> list, String keyMethod) {
		if (list == null || list.isEmpty())
			return new HashMap<K, List<T>>(0); // Collections.emptyMap() is not compatible with GWT

		Method getKey = getMethod(list.iterator().next(), keyMethod);

		Map<K, List<T>> map = new HashMap<K, List<T>>(list.size());
		// Now process each item in the list
		for (T obj : list) {
			try {
				// Invoke method to get the key
				K key = (K) getKey.invoke(obj, (Object[]) null);
				if (key == null) {
					log.warn("Retrieved key is null. Object will be ignored.");
				} else {
					List<T> mappedList = map.get(key);
					if (mappedList == null) {
						map.put(key, mappedList = new ArrayList<T>());
					}
					mappedList.add(obj);
				}
			} catch (IllegalArgumentException e) {
				log.warn(e.getMessage());
			} catch (IllegalAccessException e) {
				log.warn(e.getMessage());
			} catch (InvocationTargetException e) {
				log.warn(e.getMessage());
			}
		}

		return map;
	}

	/**
	 * Returns the first "minimum" element in the list based on each element's
	 * comparative order. Elements must be {@link Comparable}. If there are
	 * multiple minimum elements in the list, the first minimum element is returned.
	 *
	 * @param list
	 * @return
	 */
	public static <T extends Comparable<T>> T min(Collection<T> list) {
		if (list == null || list.isEmpty()) return null;

		T min = null;
		for (T element : list) {
			if (min == null || min.compareTo(element) < 0)
				min = element;
		}

		return min;
	}

	/**
	 * Returns the first "maximum" element in the list based on each element's
	 * comparative order. Elements must be {@link Comparable}. If there are
	 * multiple maximum elements in the list, the first maximum element is returned.
	 *
	 * @param list
	 * @return
	 */
	public static <T extends Comparable<T>> T max(Collection<T> list) {
		if (list == null || list.isEmpty()) return null;

		T max = null;
		for (T element : list) {
			if (max == null || max.compareTo(element) > 0)
				max = element;
		}

		return max;
	}

	/**
	 * Invokes <tt>method</tt> on each element in <tt>list</tt> and compares the
	 * return values to determine the minimum based on their comparative order.
	 * <tt>method</tt> must return a {@link Comparable} object for this to work.
	 * If there are multiple minimum values, the first minimum value is returned.s
	 *
	 * @param list
	 * @param method
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <T, M extends Comparable<M>> M min(Collection<T> list, String method) {
		if (list.isEmpty()) return null;

		Method m = getMethod(list.iterator().next(), method);

		M min = null;
		for (T element : list) {
			try {
				M value = (M) m.invoke(element, (Object[]) null);
				if (min == null) {
					min = value;
				} else {
					if (value != null && value.compareTo(min) < 0)
						min = value;
				}
			} catch (Exception e) {
				log.warn("Exception invoking '" + m.getName() + "' on '" + element + "'.");
			}
		}

		return min;
	}

	/**
	 * Returns <tt>true</tt> if all the items in <tt>objects</tt> are in the <tt>list</tt>.
	 * For each element in <tt>list</tt>, <tt>method</tt> is invoked and the result
	 * is compared to the items in <tt>objects</tt> to see if a match is found.
	 * 
	 * If <tt>list</tt> is empty, this method returns <tt>false</tt> regardless of
	 * whether <tt>objects</tt> is empty or not.
	 * 
	 * If <tt>objects</tt> is empty, this method returns <tt>true</tt> regardless of
	 * whether <tt>list</tt> is empty or not.
	 * 
	 * @param list
	 * @param method
	 * @param objects
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <T, O> boolean containsAll(Collection<T> list, String method, O... objects) {
		if (list == null || list.isEmpty())
			return false;
		
		if (objects == null || objects.length == 0)
			return true;

		Method m = getMethod(list.iterator().next(), method);

		for (T element : list) {
			try {
				O value = (O) m.invoke(element, (Object[]) null);
				boolean found = false;
				for (O object : objects) {
					if (object.equals(value)) {
						found = true;
						break;
					}
				}
				
				if (!found)
					return false;
			} catch (IllegalArgumentException e) {
				log.warn(e.getMessage());
			} catch (IllegalAccessException e) {
				log.warn(e.getMessage());
			} catch (InvocationTargetException e) {
				log.warn(e.getMessage());
			}
		}

		return true;
	}
	
	/**
	 * Returns <tt>true</tt> if any of the items in <tt>objects</tt> are in the <tt>list</tt>.
	 * For each element in <tt>list</tt>, <tt>method</tt> is invoked and the result
	 * is compared to the items in <tt>objects</tt> to see if a match is found.
	 * 
	 * If <tt>list</tt> is empty, this method returns <tt>false</tt> regardless of
	 * whether <tt>objects</tt> is empty or not.
	 * 
	 * If <tt>objects</tt> is empty, this method returns <tt>true</tt> regardless of
	 * whether <tt>list</tt> is empty or not.
	 * 
	 * @param list
	 * @param method
	 * @param objects
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <T, O> boolean containsAny(Collection<T> list, String method, O... objects) {
		if (list == null || list.isEmpty())
			return false;
		
		if (objects == null || objects.length == 0)
			return true;

		Method m = getMethod(list.iterator().next(), method);

		for (T element : list) {
			try {
				O value = (O) m.invoke(element, (Object[]) null);
				for (O object : objects) {
					if (object.equals(value)) {
						return true;
					}
				}
			} catch (IllegalArgumentException e) {
				log.warn(e.getMessage());
			} catch (IllegalAccessException e) {
				log.warn(e.getMessage());
			} catch (InvocationTargetException e) {
				log.warn(e.getMessage());
			}
		}

		return false;
	}

	// Helper Methods %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	private static <T> Method getMethod(T obj, String method) {
		Method m = null;

		try {
			// Try to get a reference to the method that will return the key for each object
			Class<?> clazz = obj.getClass();

			if (log.isDebugEnabled())
				log.trace("Processing list of " + clazz.getName() + " objects. Method: " + method);

			m = clazz.getMethod(method, (Class<?>[]) null);
		} catch (SecurityException e) {
			throw new RuntimeException(e);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(e);
		}

		return m;
	}

}
